﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using Noris.Tools.FrxEditor.Components;
using Noris.Tools.FrxEditor.ComponentsLayer;

namespace Noris.Tools.FrxEditor.Editor
{
    /// <summary>
    /// Ruler: pravítko.
    /// Nejde o potomka Control, ale o data, která akceptují pozici, měřítko a dokážou vykreslit tato data vizuálně.
    /// Tento objekt může být hostován kdekoliv.
    /// Pro vykreslení jeho dat je třeba zavolat jeho metodu Draw().
    /// Objekt je prozatím neinteraktivní.
    /// </summary>
    public class Ruler : IInteractiveObject, IDisposable
    {
        #region Data
        /// <summary>
        /// Souřadnice na vizuální ploše, kam si objekt kreslí svoje data. Vizuální koordináty (pixely).
        /// Property reaguje na změnu hodnoty: před příštím vykreslením provede přepočet.
        /// </summary>
        public RectangleF CurrentVisualBounds { get { return this._CurrentVisualBounds; } set { this._CurrentVisualBounds = value; } }
        /// <summary>
        /// Orientace pravítka (vodorovné / svislé).
        /// Property reaguje na změnu hodnoty: před příštím vykreslením provede přepočet.
        /// </summary>
        public Orientation Orientation { get { return this._Orientation; } set { this._Orientation = value; } }
        /// <summary>
        /// Barva pozadí nahoře / vlevo, vzdálenější od pracovní plochy
        /// </summary>
        public Color BackColor1 { get { return this._BackColor1; } set { this._BackColor1 = value; } }
        /// <summary>
        /// Barva pozadí dole / vpravo, blíže k pracovní ploše
        /// </summary>
        public Color BackColor2 { get { return this._BackColor2; } set { this._BackColor2 = value; } }
        private RectangleF _CurrentVisualBounds;
        private Orientation _Orientation = Orientation.Horizontal;
        private Color _BackColor1 = Color.White;
        private Color _BackColor2 = Color.LightGray;
        private string _LastCalcCode;
        #endregion
        #region Přepočet vnitřních souřadnic
        /// <summary>
        /// V případě potřeby provede rekalkulaci ticků
        /// </summary>
        /// <param name="args"></param>
        protected void Recalculate(EditEventArgs args)
        {
            // Sestavím otisk aktuálních parametrů: orientace pravítka, vizuální rozměry, logické rozměry:
            string code = this._Orientation.ToString() + ";" +
                this.CurrentVisualBounds.ToString() + ";" +
                args.ViewPortConvertor.LogicalFromVisual(this.CurrentVisualBounds).ToString();
            // Pokud máme v živé paměti výsledky kalkulace pro aktuální vstupní data, která stále platí, pak platí i výsledky:
            if (this.CurrentTicks != null && String.Equals(code, this._LastCalcCode))
                return;
            
            // Začneme přepočet:
            this._LastCalcCode = code;
            this.CurrentTicks = new List<RulerTick>();

            // Najdeme druh nejmenšího dobře viditelného ticku:
            KeyValuePair<RuleTickType, int> minStep = this._GetMinStep(args, 10f);    // Vrátí velikost kroku nejmenšího Ticku
            if (minStep.Value <= 0f)
                return;

            // Určíme souřadnici prvního viditelného ticku, a poslední viditelnou souřadnici, kde výpočet ticků skončí:
            float lastDim;                                                            // Poslední logická souřadnice ve směru pravítka
            int currStep = this._GetFirstStep(args, minStep, out lastDim);            // Vrátí pozici prvního zobrazeného ticku, vyjádřenou v desetinách logických jednotkách (mm): 1 = 0.1mm

            // Vlastní výpočet ticků:
            while (true)
            {
                // Pokud aktuální logická souřadnice je větší než koncová, kreslit ji už nebudu:
                float posLog = ((float)currStep / 10f);
                if (posLog > lastDim) break;

                // Logická a vizuální souřadnice tohoto ticku:
                float posVis = this._GetVisualPos(args, posLog);

                // Vytvořím a přidám Tick:
                RuleTickType tickType = this._GetTickType(currStep); // Vrátí typ Ticku na aktuální pozici
                string label = (tickType == minStep.Key ? "" : this._GetTickLabel(currStep, tickType));
                RulerTick rulerTick = new RulerTick(tickType, posLog, posVis, label);
                this.CurrentTicks.Add(rulerTick);

                // Další Tick
                currStep += minStep.Value;
            }
        }
        /// <summary>
        /// Vrátí pole typů ticků plus jejich číselné hodnoty, v požadovaném třídění.
        /// </summary>
        /// <param name="order">Pořadí: Ascending=od nejmenších k největším; Descending=od největším k nejmenších</param>
        /// <returns></returns>
        private List<KeyValuePair<RuleTickType, int>> _GetPreparedTicks(SortOrder order)
        {
            List<KeyValuePair<RuleTickType, int>> tickTypes = null;
            if (this.TickTypes == null)
            {   // Vytvoříme seznam typů ticku a odpovídajících hodnot (protože nyní neexistuje):
                tickTypes = new List<KeyValuePair<RuleTickType, int>>();
                Array values = Enum.GetValues(typeof(RuleTickType));
                foreach (object value in values)
                {
                    if ((int)value > 0)
                    {
                        KeyValuePair<RuleTickType, int> tick = new KeyValuePair<RuleTickType, int>((RuleTickType)value, (int)value);
                        tickTypes.Add(tick);
                    }
                }
                tickTypes.Sort((a, b) => a.Value.CompareTo(b.Value));
                this.TickTypes = tickTypes;
            }

            tickTypes = new List<KeyValuePair<RuleTickType, int>>(this.TickTypes);
            switch (order)
            {
                case SortOrder.Ascending:
                    return tickTypes;
                case SortOrder.Descending:
                    tickTypes.Reverse();
                    return tickTypes;
            }
            return null;
        }
        /// <summary>
        /// Vrátí nejmenší ze zobrazovaných ticků, vrátí jeho typ a velikost v desetinách milimetru.
        /// </summary>
        /// <param name="args"></param>
        /// <param name="minPixel">Nejmenší vzdálenost dvou sousedních ticků, které budeme zobrazovat (ticky, které by byly blíže u sebe zobrazovat nebudeme)</param>
        /// <returns></returns>
        private KeyValuePair<RuleTickType, int> _GetMinStep(EditEventArgs args, float minPixel)
        {
            // Projdeme ticky od nejjemnějších a najdeme první, který bude mít přiměřené rozestupy:
            List<KeyValuePair<RuleTickType, int>> tickTypes = this._GetPreparedTicks(SortOrder.Ascending);
            foreach (KeyValuePair<RuleTickType, int> tick in tickTypes)
            {
                float sizeL = ((float)tick.Value) / 10f;   // Velikost ticku v milimetrech (logické jednotky)
                float sizeV = args.ViewPortConvertor.VisualFromLogical(sizeL);  // Vzdálenost ticku vyjádřená v pixelech dle aktuálního zoomu
                if (sizeV >= minPixel)
                    return tick;
            }
            // Pokud nic nenajdu, vrátím 1cm:
            return new KeyValuePair<RuleTickType, int>();
        }
        /// <summary>
        /// Vrátí logickou pozici prvního zobrazeného ticku, vyjádřenou v desetinách mm (hodnota 1 = 0.1mm)
        /// </summary>
        /// <param name="args">Data editoru, obsahují ViewPort</param>
        /// <param name="minStep">Nejmenší krok vyjádřený v počtu desetin milimetru</param>
        /// <param name="lastDim"></param>
        /// <returns></returns>
        private int _GetFirstStep(EditEventArgs args, KeyValuePair<RuleTickType, int> minStep, out float lastDim)
        {
            RectangleF logicalArea = args.ViewPortConvertor.LogicalFromVisual(this.CurrentVisualBounds);
            float logicalBegin = (this._Orientation == System.Windows.Forms.Orientation.Horizontal ? logicalArea.X : logicalArea.Y);
            lastDim = (this._Orientation == System.Windows.Forms.Orientation.Horizontal ? logicalArea.Right : logicalArea.Bottom);
            int begin = (int)(Math.Ceiling(logicalBegin * 10f));    // Počátek v desetinách milimetrů, nejbližší vyšší
            int addin = begin % minStep.Value;                      // Počet desetin mm mezi počátkem pravítka a prvním tickem nejmenší hodnoty
            int first = begin + (addin == 0 ? 0 : (minStep.Value - addin));   // Logická souřadnice prvního ticku
            return first;
        }
        /// <summary>
        /// Vrátí vizuální souřadnici (pro aktuální orientaci) pro danou souřadnici logickou
        /// </summary>
        /// <param name="args"></param>
        /// <param name="posLog"></param>
        /// <returns></returns>
        private float _GetVisualPos(EditEventArgs args, float posLog)
        {
            PointF pointLog = new PointF(posLog, posLog);
            PointF pointVis = args.ViewPortConvertor.VisualFromLogical(pointLog);
            return (this._Orientation == System.Windows.Forms.Orientation.Horizontal ? pointVis.X : pointVis.Y);
        }
        /// <summary>
        /// Vrátí typ Ticku na dané souřadnici (v desetinách mm)
        /// </summary>
        /// <param name="currStep"></param>
        /// <returns></returns>
        private RuleTickType _GetTickType(int currStep)
        {
            List<KeyValuePair<RuleTickType, int>> tickTypes = this._GetPreparedTicks(SortOrder.Descending);
            foreach (KeyValuePair<RuleTickType, int> tick in tickTypes)
            {   // Procházím jednotlivé typy Ticků, seřazené sestupně podle hodnoty (Key = RuleTickType, Value = odpovídající Int32)
                if ((currStep % tick.Value) == 0)
                    return tick.Key;
            }
            return RuleTickType.None;
        }
        /// <summary>
        /// Vrátí text labelu pro daný tick (logická pozice, typ)
        /// </summary>
		/// <param name="currStep"></param>
        /// <param name="tickType"></param>
        /// <returns></returns>
        private string _GetTickLabel(int currStep, RuleTickType tickType)
        {
            int cm = currStep / 100;
            switch (tickType)
            {
                case RuleTickType.SubMilimeter:
                case RuleTickType.Milimeter:
                case RuleTickType.SubCentimeter:
                    return "";
                case RuleTickType.Centimeter:
                case RuleTickType.SubDecimeter:
                case RuleTickType.Decimeter:
                    return cm.ToString("### ##0").Trim();
            }
            return cm.ToString();
        }
		/// <summary>
		/// Seznam ticků na aktuálním pravítku
		/// </summary>
        protected List<RulerTick> CurrentTicks;
		/// <summary>
        /// Seznam hodnot: pro typ ticku obsahuje jeho velikost v desetinách milimetru, jednou připravený a poté již konstantní.
        /// Třídění tohoto seznamu = Ascending.
		/// </summary>
        protected List<KeyValuePair<RuleTickType, int>> TickTypes;
		/// <summary>
		/// Jeden tick na pravítku, kompletní data
		/// </summary>
        protected class RulerTick
        {
            internal RulerTick(RuleTickType tickType, float positionLogical, float positionVisual, string label)
            {
                this.TickType = tickType;
                this.PositionLogical = positionLogical;
                this.PositionVisual = positionVisual;
                this.Label = label;
            }
			/// <summary>
			/// Vizualiace ticku
			/// </summary>
			/// <returns></returns>
            public override string ToString()
            {
                return this.TickType.ToString() + ": " + this.Label + " cm    {Log:" + this.PositionLogical.ToString() + "mm, Vis:" + this.PositionVisual.ToString() + "px }";
            }
            internal RuleTickType TickType { get; private set; }
            internal float PositionLogical { get; private set; }
            internal float PositionVisual { get; private set; }
            internal string Label { get; private set; }
        }
		/// <summary>
		/// Typy ticku = značky na pravítku.
        /// Hodnota Int32 tohoto enumu odpovídá desetinám milimetru dané značky, takže například Milimeter = 10.
		/// </summary>
        protected enum RuleTickType
        {
			/// <summary>
			/// Žádné
			/// </summary>
            None = 0,
            /// <summary>
            /// Miniatura = půlmilimetr
            /// </summary>
            SubMilimeter = 5,
            /// <summary>
            /// Milimetr
            /// </summary>
            Milimeter = 10,
            /// <summary>
            /// Půlcentimetr
            /// </summary>
            SubCentimeter = 50,
            /// <summary>
            /// Centimetr
            /// </summary>
            Centimeter = 100,
            /// <summary>
            /// 5 centimetrů
            /// </summary>
            SubDecimeter = 500,
            /// <summary>
            /// 10 centimetrů
            /// </summary>
            Decimeter = 1000
        }
        #endregion
        #region Kreslení
        /// <summary>
        /// Vykreslí pravítko
        /// </summary>
        /// <param name="args"></param>
        public void Paint(EditPaintEventArgs args)
        {
            this.Recalculate(args);
            this.DrawBackground(args);
            if (this.CurrentTicks != null && this.CurrentTicks.Count > 0)
                this.DrawTicks(args);
        }
        /// <summary>
        /// Vykreslí pozadí pravítka
        /// </summary>
        /// <param name="args"></param>
        protected virtual void DrawBackground(EditPaintEventArgs args)
        {
            using (Brush back = this.GetBackBrush())
            {
                args.Graphics.FillRectangle(back, this._CurrentVisualBounds);
            }
        }
        /// <summary>
        /// Vrátí Brush pro kreslení pozadí pravítka
        /// </summary>
        /// <returns></returns>
        protected virtual Brush GetBackBrush()
        {
            Color color1 = this.BackColor1;
            Color color2 = this.BackColor2;
            System.Drawing.Drawing2D.LinearGradientBrush lgb;
            switch (this.Orientation)
            {
                case System.Windows.Forms.Orientation.Horizontal:
                    lgb = new System.Drawing.Drawing2D.LinearGradientBrush(this._CurrentVisualBounds, color1, color2, 90f);
                    return lgb;
                case System.Windows.Forms.Orientation.Vertical:
                    lgb = new System.Drawing.Drawing2D.LinearGradientBrush(this._CurrentVisualBounds, color1, color2, 0f);
                    return lgb;
            }
            return new SolidBrush(color1);
        }
        /// <summary>
        /// Vykreslí ticky pravítka
        /// </summary>
        /// <param name="args"></param>
        protected void DrawTicks(EditPaintEventArgs args)
        {
            if (this.TickDataDict == null || this.TickDataDict.Count == 0)
                this.PrepareTickData();

            switch (this.Orientation)
            {
                case System.Windows.Forms.Orientation.Horizontal:
                    this.DrawTicksHorizontal(args);
                    break;
                case System.Windows.Forms.Orientation.Vertical:
                    this.DrawTicksVertical(args);
                    break;
            }
        }
        /// <summary>
        /// Vykreslí ticky pravítka typu Horizontal
        /// </summary>
        /// <param name="args"></param>
        protected void DrawTicksHorizontal(EditPaintEventArgs args)
        {
            float fontEnd = this._CurrentVisualBounds.Bottom - 8f;            // Konec fontu (spodní okraj)
            float tickEnd = this._CurrentVisualBounds.Bottom - 2f;            // Konec ticku (spodní okraj)
            foreach (RulerTick tick in this.CurrentTicks)
            {
                RuleTickData tickData = this.TickDataDict[tick.TickType];
                if (tickData.TickPen == null) continue;                  // Tento tick se nemá kreslit
                args.Graphics.DrawLine(tickData.TickPen, tick.PositionVisual, tickEnd - tickData.TickLength, tick.PositionVisual, tickEnd);
                if (!String.IsNullOrEmpty(tick.Label) && tickData.TickFont != null)
                {
                    SizeF labelSize = args.Graphics.MeasureString(tick.Label, tickData.TickFont);
                    PointF labelPoint = new PointF(tick.PositionVisual - labelSize.Width / 2f, fontEnd - tickData.TickFont.Height);
                    RectangleF labelArea = new RectangleF(labelPoint, labelSize);
                    args.Graphics.DrawString(tick.Label, tickData.TickFont, tickData.FontBrush, labelArea);
                }
            }
        }
        /// <summary>
        /// Vykreslí ticky pravítka typu Vertical
        /// </summary>
        /// <param name="args"></param>
        protected void DrawTicksVertical(EditPaintEventArgs args)
        {
            float fb = this._CurrentVisualBounds.Bottom - 8f;            // Bottom fontu
            float tb = this._CurrentVisualBounds.Bottom;
            foreach (RulerTick tick in this.CurrentTicks)
            {
                RuleTickData tickData = this.TickDataDict[tick.TickType];
                if (tickData.TickPen == null) continue;                  // Tento tick se nemá kreslit
                args.Graphics.DrawLine(tickData.TickPen, tick.PositionVisual, tb - tickData.TickLength, tick.PositionVisual, tb);
                if (!String.IsNullOrEmpty(tick.Label) && tickData.TickFont != null)
                {
                    SizeF labelSize = args.Graphics.MeasureString(tick.Label, tickData.TickFont);
                    PointF labelPoint = new PointF(tick.PositionVisual - labelSize.Width / 2f, fb - tickData.TickFont.Height);
                    RectangleF labelArea = new RectangleF(labelPoint, labelSize);
                    args.Graphics.DrawString(tick.Label, tickData.TickFont, tickData.FontBrush, labelArea);
                }
            }
        }
		/// <summary>
		/// Připraví data všech tisků do indexu this.TickDataDict
		/// </summary>
        protected void PrepareTickData()
        {
            this.TickDataDict = new Dictionary<RuleTickType, RuleTickData>();
            this.TickDataDict.Add(RuleTickType.None, new RuleTickData(RuleTickType.None, 0f, Color.Empty, 0f, 0f, FontStyle.Regular));
            this.TickDataDict.Add(RuleTickType.SubMilimeter, new RuleTickData(RuleTickType.SubMilimeter, 0f, Color.Empty, 0f, 0f, FontStyle.Regular));
            this.TickDataDict.Add(RuleTickType.Milimeter, new RuleTickData(RuleTickType.Milimeter, 3f, Color.Gray, 1f, 0f, FontStyle.Regular));
            this.TickDataDict.Add(RuleTickType.SubCentimeter, new RuleTickData(RuleTickType.SubCentimeter, 4f, Color.Gray, 1f, 0f, FontStyle.Regular));
            this.TickDataDict.Add(RuleTickType.Centimeter, new RuleTickData(RuleTickType.Centimeter, 5f, Color.Gray, 1f, 7f, FontStyle.Regular));
            this.TickDataDict.Add(RuleTickType.SubDecimeter, new RuleTickData(RuleTickType.SubDecimeter, 8f, Color.Black, 2f, 7f, FontStyle.Regular));
            this.TickDataDict.Add(RuleTickType.Decimeter, new RuleTickData(RuleTickType.Decimeter, 9f, Color.Black, 2f, 8f, FontStyle.Bold));
        }
        /// <summary>
        /// Index obsahující data všech typů ticků
        /// </summary>
        protected Dictionary<RuleTickType, RuleTickData> TickDataDict;
        #endregion
        #region Interaktivita
        /// <summary>
        /// Informace o změně stavu a pozice myši.
        /// Objekt může reagovat podle své potřeby, a může žádat o překreslení určité vrstvy controlu + cursoru.
        /// </summary>
        /// <param name="args"></param>
        public void MouseChange(EditInteractiveEventArgs args)
        {
        }
        #endregion
        #region class RuleTickData : Data jednoho typu ticku
        /// <summary>
        /// Data jednoho typu ticku
        /// </summary>
        protected class RuleTickData : IDisposable
        {
            /// <summary>
            /// Data jednoho typu ticku. Konstruktor.
            /// </summary>
            /// <param name="tickType"></param>
            /// <param name="tickLength"></param>
            /// <param name="penColor"></param>
            /// <param name="penWidth"></param>
            /// <param name="fontSize"></param>
            /// <param name="fontStyle"></param>
            internal RuleTickData(RuleTickType tickType, float tickLength, Color penColor, float penWidth, float fontSize, FontStyle fontStyle)
            { 
                this.TickType = tickType;
                this.TickLength = tickLength;
                if (penWidth > 0f)
                    this.TickPen = new Pen(penColor, penWidth);
                if (fontSize > 0f)
                {
                    this.TickFont = new Font(SystemFonts.DefaultFont.FontFamily, fontSize, fontStyle);
                    this.FontBrush = new SolidBrush(penColor);
                }
            }
            /// <summary>
            /// Typ ticku
            /// </summary>
            internal RuleTickType TickType { get; private set; }
            /// <summary>
            /// Délka ticku (úsečka)
            /// </summary>
            internal float TickLength { get; private set; }
            /// <summary>
            /// Pero pro kreslení úsečky (obsahuje přiměřenou barvu a šířku)
            /// </summary>
            internal Pen TickPen { get; private set; }
            /// <summary>
            /// Font pro psaní labelu, může být null
            /// </summary>
            internal Font TickFont { get; private set; }
            /// <summary>
            /// Barva (výplň fontu) pro psaní labelu, může být null
            /// </summary>
            internal Brush FontBrush { get; private set; }
            /// <summary>
            /// Dispose
            /// </summary>
            public void Dispose()
            {
                if (this.TickPen != null)
                    this.TickPen.Dispose();
                this.TickPen = null;

                if (this.TickFont != null)
                    this.TickFont.Dispose();
                this.TickFont = null;

                if (this.FontBrush != null)
                    this.FontBrush.Dispose();
                this.FontBrush = null;
            }
        }
        #endregion
        #region enum SortOrder
        /// <summary>
        /// Pořadí třídění
        /// </summary>
        protected enum SortOrder
        {
            /// <summary>
            /// Vzestupně
            /// </summary>
            Ascending,
            /// <summary>
            /// Sestupně
            /// </summary>
            Descending
        }
        #endregion
        #region IDisposable
        /// <summary>
        /// IDisposable
        /// </summary>
        public void Dispose()
        {
            foreach (RuleTickData tickData in this.TickDataDict.Values)
                tickData.Dispose();
            this.TickDataDict = null;
        }
        #endregion
    }
}
